package tpavel.proxy;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Proxy implements Runnable {
	Logger logger = LoggerFactory.getLogger(ConsoleApp.class);
	
	private ProxyServiceProperties properties;
	
	private final ByteBuffer buffer = ByteBuffer.allocate(16384);

	public Proxy(ProxyServiceProperties properties) {
		this.properties = properties;
	}

	/**
	 * Main cycle process socket
	 */
	@Override
	public void run() {
		logger.info(String.format("Start service %s", properties.toString()));

		try {
			// Instead of creating a ServerSocket,
			// create a ServerSocketChannel
			ServerSocketChannel ssc = ServerSocketChannel.open();

			// Set it to non-blocking, so we can use select
			ssc.configureBlocking(false);

			// Get the Socket connected to this channel, and bind it
			// to the listening port
			ServerSocket ss = ssc.socket();
			InetSocketAddress isa = new InetSocketAddress(getLocalPort());
			ss.bind(isa);

			// Create a new Selector for selecting
			Selector selector = Selector.open();

			// Register the ServerSocketChannel, so we can
			// listen for incoming connections
			ssc.register(selector, SelectionKey.OP_ACCEPT);
			logger.debug("Listening on port " + getLocalPort());

			while (true) {
				// See if we've had any activity -- either
				// an incoming connection, or incoming data on an
				// existing connection
				int num = selector.select();

				// If we don't have any activity, loop around and wait
				// again
				if (num == 0) {
					continue;
				}

				// Get the keys corresponding to the activity
				// that has been detected, and process them
				// one by one
				Set keys = selector.selectedKeys();
				Iterator it = keys.iterator();
				while (it.hasNext()) {
					// Get a key representing one of bits of I/O
					// activity
					SelectionKey key = (SelectionKey) it.next();

					// What kind of activity is it?
					if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {

						logger.debug("acc");
						// It's an incoming connection.
						// Register this socket with the Selector
						// so we can listen for input on it

						Socket s = ss.accept();
						logger.debug("Got connection from " + s);

						// Make sure to make it non-blocking, so we can
						// use a selector on it.
						SocketChannel sc = s.getChannel();
						sc.configureBlocking(false);

						// Register it with the selector, for reading
						sc.register(selector, SelectionKey.OP_READ);
					} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {

						SocketChannel sc = null;

						try {

							// It's incoming data on a connection, so
							// process it
							sc = (SocketChannel) key.channel();
							boolean ok = processInput(sc);

							// If the connection is dead, then remove it
							// from the selector and close it
							if (!ok) {
								key.cancel();

								Socket s = null;
								try {
									s = sc.socket();
									s.close();
								} catch (IOException ie) {
									System.err.println("Error closing socket "
											+ s + ": " + ie);
								}
							}

						} catch (IOException ie) {

							// On exception, remove this channel from the
							// selector
							key.cancel();

							try {
								sc.close();
							} catch (IOException ie2) {
								System.out.println(ie2);
							}

							System.out.println("Closed " + sc);
						}
					}
				}

				// We remove the selected keys, because we've dealt
				// with them.
				keys.clear();
			}
		} catch (IOException ie) {
			System.err.println(ie);
		}
		
		logger.info(String.format("Down service %s", properties.toString()));
	}

	private boolean processInput(SocketChannel sc) throws IOException {
		
		buffer.clear();
		sc.read(buffer);
		buffer.flip();

		// If no data, close the connection
		if (buffer.limit() == 0) {
			return false;
		}

		// Simple rot-13 encryption
		for (int i = 0; i < buffer.limit(); ++i) {
			byte b = buffer.get(i);

			if ((b >= 'a' && b <= 'm') || (b >= 'A' && b <= 'M')) {
				b += 13;
			} else if ((b >= 'n' && b <= 'z') || (b >= 'N' && b <= 'Z')) {
				b -= 13;
			}

			buffer.put(i, b);
		}

//		sc.write(buffer);

		System.out.println("Processed " + buffer.limit() + " from " + sc);

		
		return true;
	}

	/**
	 * Start proxy
	 */
	public void start() {
		new Thread(this).start();
	}
	
	/**
	 * get local port
	 * @return port
	 */
	private int getLocalPort() {
		return properties.getLocalPort();
	}

}
